 /*******************************************************************************
  * Copyright (c) 2004 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Common Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/cpl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/
 package org.eclipse.osgi.service.datalocation;

 import java.io.*;
 import java.util.*;
 import org.eclipse.core.runtime.adaptor.*;
 import org.eclipse.core.runtime.adaptor.BasicLocation;
 import org.eclipse.core.runtime.adaptor.Locker;

 /**
  * File managers provide a facility for tracking the state of files being used and updated by several
  * systems at the same time. The typical usecase is in shared configuration data areas.
  * <p>
  * The general principle is to maintain a table which maps user-level file name
  * onto actual disk file. The filename is actually never used, and the file is always stored under the
  * given filename suffixed by an integer. If a file needs to be modified, it is written into a new file whose name suffix
  * is incremented.
  * Once the instance has been created, open() must be called before performing any other operation.
  * On open the fileManager starts by reading the current table and
  * thereby obtaining a snapshot of the current directory state. If another
  * entity updates the directory, the file manager is able to detect the change.
  * Given that the file is unique, if another entity used the file manager mechanism, the file manager can
  * still access the state of the file as it was when the file manager first started.
  * <p>
  * The facilities provided here are cooperative. That is, all participants must
  * agree to the conventions and to calling the given API. There is no capacity
  * to enforce these conventions or prohibit corruption.
  * </p>
  */
 public class FileManager {
     private class Entry {
         int readId;
         int writeId;

         Entry(int readId, int writeId) {
             this.readId = readId;
             this.writeId = writeId;
         }

         int getReadId() {
             return readId;
         }

         int getWriteId() {
             return writeId;
         }

         void setReadId(int value) {
             readId = value;
         }

         void setWriteId(int value) {
             writeId = value;
         }
     }

     private File base; //The folder managed
 private File managerRoot; //The folder that will contain all the file related to the functionning of the manager (typically a subdir of base)

     private String lockMode = null;
     private File tableFile = null;
     private File lockFile; // The lock file for the table (this file is the same for all the instances)
 private Locker locker; // The locker for the lock

     private File instanceFile = null; //The file reprensenting the running instance. It is created when the table file is read.
 private Locker instanceLocker = null; //The locker for the instance file.

     // locking related fields
 private long tableStamp = 0L;

     private Properties table = new Properties();

     private static final String MANAGER_FOLDER = ".manager"; //$NON-NLS-1$
 private static final String TABLE_FILE = ".fileTable"; //$NON-NLS-1$
 private static final String LOCK_FILE = ".fileTableLock"; //$NON-NLS-1$

     /**
      * Returns a new file manager for the area identified by the given base
      * directory.
      *
      * @param base the directory holding the files to be managed
      * @param lockMode the lockMode to use for the given filemanager. It can have one the 3 values: none, java.io, java.nio
      * and also supports null in which case the lock strategy will be the global one.
      */
     public FileManager(File base, String lockMode) {
         this.base = base;
         this.lockMode = lockMode;
         this.managerRoot = new File(base, MANAGER_FOLDER);
         this.managerRoot.mkdir();
         this.tableFile = new File(managerRoot, TABLE_FILE);
         this.lockFile = new File(managerRoot, LOCK_FILE);
     }

     private void initializeInstanceFile() throws IOException {
         if (instanceFile != null)
             return;
         this.instanceFile = File.createTempFile(".tmp", ".instance", managerRoot); //$NON-NLS-1$//$NON-NLS-2$
 this.instanceFile.deleteOnExit();
         instanceLocker = BasicLocation.createLocker(instanceFile, lockMode);
         instanceLocker.lock();
     }

     private String getAbsolutePath(String file) {
         return new File(base, file).getAbsolutePath();
     }

     /**
      * Add the given file name to the list of files managed by this location.
      *
      * @param file path of the file to manage
      * @throws IOException if there are any problems adding the given file to the manager
      */
     public void add(String file) throws IOException {
         if (! lock())
             throw new IOException(EclipseAdaptorMsg.formatter.getString("fileManager.cannotLock")); //$NON-NLS-1$
 try {
             updateTable();
             Entry entry = (Entry) table.get(file);
             if (entry == null) {
                 table.put(file, new Entry(0, 1));
                 save();
             }
         } finally {
             release();
         }
     }

     /**
      * Update the given target files with the content in the given source files.
      * The targets are file paths which are currently managed. The sources are
      * absolute (or relative to the current working directory) file paths
      * containing the new content for the corresponding target.
      *
      * @param targets the target files to update
      * @param sources the new content for the target files
      * @throws IOException if there are any problems updating the given files
      */
     public void update(String [] targets, String [] sources) throws IOException {
         if (! lock())
             throw new IOException(EclipseAdaptorMsg.formatter.getString("fileManager.cannotLock")); //$NON-NLS-1$;
 try {
             updateTable();
             for (int i = 0; i < targets.length; i++) {
                 String target = targets[i];
                 update(target, sources[i]);
             }
             save();
         } finally {
             release();
         }
     }

     /**
      * Returns a list of all the file paths currently being managed.
      *
      * @return the file paths being managed
      */
     public String [] getFiles() {
         Set set = table.keySet();
         String [] keys = (String []) set.toArray(new String [set.size()]);
         String [] result = new String [keys.length];
         for (int i = 0; i < keys.length; i++)
             result[i] = new String (keys[i]);
         return result;
     }

     /**
      * Returns the directory containing the files being managed by this file
      * manager.
      *
      * @return the directory containing the managed files
      */
     public File getBase() {
         return base;
     }

     /**
      * Returns the current numeric id (appendage) of the given file.
      * <code>file + "." + getId(file)</code>. -1 is returned if the given
      * target file is not managed.
      *
      * @param target
      * the managed file to access
      * @return the id of the file
      */
     public int getId(String target) {
         Entry entry = (Entry) table.get(target);
         if (entry == null)
             return -1;
         return entry.getReadId();
     }

     /**
      * Attempts to lock the state of this manager and returns <code>true</code>
      * if the lock could be acquired.
      * <p>
      * Locking a manager is advisory only. That is, it does not prevent other
      * applications from modifying the files managed by this manager.
      * </p>
      *
      * @exception IOException
      * if there was an unexpected problem while acquiring the
      * lock.
      */
     private boolean lock() throws IOException {
         if (locker == null)
             locker = BasicLocation.createLocker(lockFile, lockMode);
         if (locker == null)
             throw new IOException(EclipseAdaptorMsg.formatter.getString("fileManager.cannotLock")); //$NON-NLS-1$
 return locker.lock();
     }

     /**
      * Returns the actual file location to use when reading the given managed file.
      * <code>null</code> can be returned if the given target is not managed and add is set to false.
      *
      * @param target the managed file to lookup
      * @param add indicate whether the file should be added to the manager if it is not managed.
      * @throws IOException if the add flag is set to true and the addition of the file failed
      * @return the absolute file location to use for the given file or
      * <code>null</code> if the given target is not managed
      */
     public File lookup(String target, boolean add) throws IOException {
         Entry entry = (Entry) table.get(target);
         if (entry == null) {
             if (add) {
                 add(target);
                 entry = (Entry) table.get(target);
             } else {
                 return null;
             }
         }
         return new File(getAbsolutePath(target + '.' + entry.getReadId()));
     }

     private void move(String source, String target) {
         File original = new File(source);
         // its ok if the original does not exist. The table entry will capture
 // that fact. There is no need to put something in the filesystem.
 if (!original.exists())
             return;
         original.renameTo(new File(target));
     }

     /**
      * Saves the state of the file manager and releases any locks held.
      */
     private void release() {
         if (locker == null)
             return;
         locker.release();
     }

     /**
      * Removes the given file from management by this file manager.
      *
      * @param file the file to remove
      */
     public void remove(String file) throws IOException {
         // The removal needs to be done eagerly, so the value is effectively removed from the disktable.
 // Otherwise, an updateTable() caused by an update(,) could cause the file to readded to the local table.
 if (! lock())
             throw new IOException(EclipseAdaptorMsg.formatter.getString("fileManager.cannotLock")); //$NON-NLS-1$;
 try {
             updateTable();
             table.remove(file);
             save();
         } finally {
             release();
         }
     }

     private void updateTable() throws IOException {
         if (!tableFile.exists())
             return;
         long stamp = tableFile.lastModified();
         if (stamp == tableStamp)
             return;
         initializeInstanceFile();
         Properties diskTable = new Properties();
         try {
             FileInputStream input = new FileInputStream(tableFile);
             try {
                 diskTable.load(input);
             } finally {
                 input.close();
             }
         } catch (IOException e) {
             throw e; // rethrow the exception, we have nothing to add here
 }
         tableStamp = stamp;
         for (Enumeration e = diskTable.keys(); e.hasMoreElements();) {
             String file = (String ) e.nextElement();
             String readNumber = diskTable.getProperty(file);
             if (readNumber != null) {
                 Entry entry = (Entry) table.get(file);
                 int id = Integer.parseInt(readNumber);
                 if (entry == null) {
                     table.put(file, new Entry(id, id + 1));
                 } else {
                     entry.setWriteId(id + 1);
                 }
             }
         }
     }

     /*
      * This method should be called while the manager is locked.
      */
     private void save() throws IOException {
         // if the table file has change on disk, update our data structures then
 // rewrite the file.
 if (tableStamp != tableFile.lastModified())
             updateTable();
         Properties props = new Properties();
         for (Enumeration e = table.keys(); e.hasMoreElements();) {
             String file = (String ) e.nextElement();
             Entry entry = (Entry) table.get(file);
             String value = Integer.toString(entry.getWriteId() - 1); //In the table we save the write number - 1, because the read number can be totally different.
 props.put(file, value);
         }
         FileOutputStream fileStream = new FileOutputStream(tableFile);
         try {
             try {
                 props.store(fileStream, "safe table"); //$NON-NLS-1$
 } finally {
                 fileStream.close();
             }
         } catch (IOException e) {
             throw new IOException(EclipseAdaptorMsg.formatter.getString("fileManager.couldNotSave")); //$NON-NLS-1$
 }
     }

     private void update(String target, String source) {
         Entry entry = (Entry) table.get(target);
         int newId = entry.getWriteId();
         move(getAbsolutePath(source), getAbsolutePath(target) + '.' + newId);
         // update the entry. read and write ids should be the same since
 // everything is in sync
 entry.setReadId(newId);
         entry.setWriteId(newId + 1);
     }

     /**
      * This methods remove all the temporary files that have been created by the fileManager.
      * This removal is only done if the instance of eclipse calling this method is the last instance using this fileManager.
      * @throws IOException
      */
     private void cleanup() throws IOException {
         //Iterate through the temp files and delete them all, except the one representing this filemanager.
 String [] files = managerRoot.list();
         for (int i = 0; i < files.length; i++) {
             if (files[i].endsWith(".instance") && !files[i].equalsIgnoreCase(instanceFile.getName())) { //$NON-NLS-1$
 if (new File(managerRoot, files[i]).delete() == false)
                     return;
             }
         }

         //If we are here it is because we are the last instance running. After locking the table and getting its latest content, remove all the backup files and change the table
 //If the exception comes from lock, another instance may have been started after we cleaned up, therefore we abort
 if (! lock())
             throw new IOException(EclipseAdaptorMsg.formatter.getString("fileManager.cannotLock")); //$NON-NLS-1$
 try {
             updateTable();
             Collection managedFiles = table.entrySet();
             for (Iterator iter = managedFiles.iterator(); iter.hasNext();) {
                 Map.Entry fileEntry = (Map.Entry) iter.next();
                 String fileName = (String ) fileEntry.getKey();
                 Entry info = (Entry) fileEntry.getValue();
                 //Because we are cleaning up, we are giving up the values from our table, and we must delete all the files that are not referenced by the table
 String readId = Integer.toString(info.getWriteId() - 1);
                 deleteCopies(fileName, readId);
             }
         } catch (IOException e) {
             //If the exception comes from the updateTable(), there has been a problem in reading the file.
 //If an exception occured in the save, then the table won't be up to date!
 throw e;
         } finally {
             release();
         }
     }

     private void deleteCopies(String fileName, String exceptionNumber) {
         String notToDelete = fileName + '.' + exceptionNumber;
         String [] files = base.list();
         for (int i = 0; i < files.length; i++) {
             if (files[i].startsWith(fileName + '.') && !files[i].equals(notToDelete)) //$NON-NLS-1$
 new File(base, files[i]).delete();
         }
     }

     /**
      * This methods declare the fileManager as closed. From thereon, the instance can no longer be used.
      * It is important to close the manager as it also cleanup old copies of the managed files.
      */
     public void close() {
         try {
             cleanup();
         } catch (IOException e) {
             //Ignore and close.
 }
         instanceLocker.release();
         instanceFile.delete();
     }

     /**
      * This methods opens the fileManager, which loads the table in memory. This method must be called before any operation on the filemanager.
      * @param wait indicates if the open operation must wait in case of contention on the lock file.
      */
     public void open(boolean wait) throws IOException {
         boolean locked = lock();
         if (! locked && wait==false)
             throw new IOException(EclipseAdaptorMsg.formatter.getString("fileManager.cannotLock")); //$NON-NLS-1$;

         //wait for the lock to be released
 if (! locked) {
             do {
                 try {
                     Thread.sleep(10);
                 } catch (InterruptedException e) {
                     // Ignore the exception and keep waiting
 }
             } while(lock());
         }
         
         try {
             updateTable();
         } finally {
             release();
         }
     }
 }
